<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Ldap
 * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Node.php 10 2008-11-08 17:17:02Z Stefan.Gehrig.HN $
 */

/** 
 * Zend_Ldap_Ext 
 */
require_once 'Zend/Ldap/Ext.php';
/** 
 * Zend_Ldap_Exception
 */
require_once 'Zend/Ldap/Exception.php';
/** 
 * Zend_Ldap_Helper 
 */
require_once 'Zend/Ldap/Helper.php';

/**
 * Zend_Ldap_Node provides an object oriented view into a LDAP node.
 *
 * @category   Zend
 * @package    Zend_Ldap
 * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Ldap_Node implements ArrayAccess, IteratorAggregate, Countable 
{
    /**
     * Holds the node's DN.
     *
     * @var array
     */
    private $_dn;
    /**
     * Holds the node's attributes.
     *
     * @var array
     */
    private $_data;
    /**
     * Holds the node's changed data.
     *
     * @var array
     */
    private $_changed;
    /**
     * Holds the connection to the LDAP server if in connected mode.
     *
     * @var Zend_Ldap_Ext
     */
    private $_ldap;
    /**
     * Caches the child nodes of the current node.
     *
     * @var Zend_Ldap_Node_Children
     */
    private $_children;
    
    /**
     * Creates a new Zend_Ldap_Node
     * 
     * Constructor is private to enforce the use of factory methods.
     *
     * @param  array $data
     * @param  Zend_Ldap_Ext $ldap
     * @throws Zend_Ldap_Exception
     */
    private function __construct(array $data, Zend_Ldap_Ext $ldap=null)
    {
        if (!array_key_exists('dn', $data) || !is_string($data['dn'])) {
            throw new Zend_Ldap_Exception(null, 'Invalid data array format. Key \'dn\' missing or malformed.', 
                Zend_Ldap_Exception::LDAP_OTHER);
        }
        $this->_loadData($data);
        if (!is_null($ldap)) $this->attachLdap($ldap);
        else $this->detachLdap();        
    }
    
    /**
     * Serialization callback
     *
     * Only node's children and attributes will be serialized.
     * 
     * @return array
     */
    public function __sleep()
    {
        return array('_dn', '_data', '_changed');
    }
    
    /**
     * Deserialization callback
     *
     * Enforces a detached node.
     * 
     * @return null
     */
    public function __wakeup()
    {
        $this->detachLdap();
    }
    
    /**
     * Gets the current LDAP connection.
     * 
     * @return Zend_Ldap_Ext
     * @throws Zend_Ldap_Exception
     */
    public function getLdap()
    {
        if (is_null($this->_ldap)) 
            throw new Zend_Ldap_Exception(null, 'No LDAP connection specified.', Zend_Ldap_Exception::LDAP_OTHER);
        else return $this->_ldap;
    }
    
    /**
     * @param array $data
     * @throws Zend_Ldap_Exception
     */
    private function _loadData(array $data)
    {
        $keys=array();
        $values=array();
        $dnData=Zend_Ldap_Helper::explodeDn($data['dn'], $keys, $values);
        $this->_dn=array();
        $this->_dn['string']=$data['dn'];
        $this->_dn['array']=$dnData;
        $this->_dn['keys']=$keys;
        $this->_dn['values']=$values;
        $this->_children=null;
        $this->_changed=array();
        unset($data['dn']);
        $this->_data=$data;
    }
    
    /**
     * Attach node to an LDAP connection
     * 
     * This is an offline method.
     *
     * @uses   Zend_Ldap_Helper::isChildOf()
     * @param  Zend_Ldap_Ext $ldap
     * @return null
     * @throws Zend_Ldap_Exception
     */
    public function attachLdap(Zend_Ldap_Ext $ldap)
    {
        if (!Zend_Ldap_Helper::isChildOf($this->getDn(), $ldap->getBaseDn())) {
            throw new Zend_Ldap_Exception(null, 'LDAP connection is not responsible for given node.', 
                Zend_Ldap_Exception::LDAP_OTHER); 
        }
        $this->_ldap=$ldap; 
    }
    
    /**
     * Detach node from LDAP connection
     * 
     * This is an offline method.
     * 
     * @return null
     */
    public function detachLdap()
    {
        $this->_ldap=null;
        $this->_children=null;
    }
    
    /**
     * Checks if the current node is attached to a LDAP server.
     *
     * This is an offline method.
     * 
     * @return boolean
     */
    public function isAttached()
    {
        return (!is_null($this->_ldap));
    }
    
    /**
     * Factory method to create a new detached Zend_Ldap_Node for a given DN.
     *
     * @param  string $dn
     * @param  array $objectClass
     * @param  string $encoding
     * @return Zend_Ldap_Node
     * @throws Zend_Ldap_Exception
     */
    public static function create($dn, array $objectClass=null, $encoding='UTF-8')
    {
        $data=array('dn' => $dn);
        $new=new self($data, null);
        $keys=$new->getDnKeys();
        $vals=$new->getDnValues();
        $new->_setAttribute($keys[0], $vals[0], $encoding);
        if (!is_null($objectClass)) $new->setAttribute('objectClass', $objectClass);
        return $new;
    }
    
    /**
     * Factory method to create an attached Zend_Ldap_Node for a given DN.
     *
     * @param  string $dn
     * @param  Zend_Ldap_Ext $ldap
     * @return Zend_Ldap_Node
     * @throws Zend_Ldap_Exception
     */
    public static function fromLdap($dn, Zend_Ldap_Ext $ldap)
    {
        $data=$ldap->getEntry($dn, array('*', '+'), true);
        return new self($data, $ldap);
    }
    
    /**
     * Factory method to create a detached Zend_Ldap_Node from array data.
     *
     * @param  array $data
     * @return Zend_Ldap_Node
     * @throws Zend_Ldap_Exception
     */
    public static function fromArray(array $data)
    {
        return new self($data, null);
    }
    
    /**
     * Creates a new node by copying the current node and
     * assigning a new DN.
     *
     * @param string $newDn
     * @param string $encoding
     * @return Zend_Ldap_Node
     * @throws Zend_Ldap_Exception
     */
    public function copyNode($newDn, $encoding='UTF-8')
    {
        $new=self::create($newDn, null, $encoding);
        $new->_changed=array_merge($this->_data, $new->_changed);
        if ($this->isAttached()) $new->attachLdap($this->getLdap());
        return $new;
    }
    
    /**
     * Gets the DN of the current node.
     *
     * This is an offline method.
     * 
     * @return string
     */
    public function getDn()
    {
        return $this->_dn['string'];
    }
    
    /**
     * Gets the DN of the current node as an array.
     *
     * This is an offline method.
     * 
     * @return array
     */
    public function getDnArray()
    {
        return $this->_dn['array'];
    }
    
    /**
     * Gets the DN keys of the current node as an array.
     *
     * This is an offline method.
     * 
     * @return array
     */
    public function getDnKeys()
    {
        return $this->_dn['keys'];
    }
    
    /**
     * Gets the DN values of the current node as an array.
     *
     * This is an offline method.
     * 
     * @return array
     */
    public function getDnValues()
    {
        return $this->_dn['values'];
    }
    
    /**
     * Gets the RDN of the current node.
     *
     * This is an offline method.
     * 
     * @return string
     */
    public function getRdn()
    {
        $dnArray=$this->getDnArray();
        return Zend_Ldap_Helper::implodeRdn($dnArray[0]);
    }
    
    /**
     * Gets node data.
     *
     * The array contains all attributes and the DN with key 'dn'.
     * This is the format used by {@link Zend_Ldap_Helper::getAttribute()}.
     * 
     * This is an offline method.
     * 
     * @return array
     */
    public function getData()
    {
        $data=$this->getAttributes();
        $data['dn']=$this->getDn();
        return $data;
    }
    
    /**
     * Gets all attributes of node.
     *
     * The array contains all attributes. The DN is stripped from the array.
     * This format can be used in {@link Zend_Ldap_Ext::add()} and {@link Zend_Ldap_Ext::update()},
     * although both methods can be used with {@link getData()} as both methods
     * strip the 'dn' key.
     * 
     * This is an offline method.
     * 
     * @return array
     */
    public function getAttributes()
    {
        return array_merge($this->_data, $this->_changed);
    }
    
    /**
     * Gets all attribute keys of node.
     *
     * The array contains all attribute keys. 
     * 
     * This is an offline method.
     * 
     * @return array
     */
    public function getAttributeKeys()
    {
        return array_keys($this->getAttributes());
    }
    
    /**
     * Returns the number of attributes in node.
     * Implements Countable
     * 
     * @return int
     */
    public function count()
    {
        return count($this->getAttributes());
    }
    
    /**
     * Gets changed attributes of node.
     *
     * The array contains all changed attributes. The DN is stripped from the array.
     * This format can be used in {@link Zend_Ldap_Ext::add()} and {@link Zend_Ldap_Ext::update()},
     * although both methods can be used with {@link getChangedData()} as both methods
     * strip the 'dn' key.
     * 
     * This is an offline method.
     * 
     * @return array
     */
    public function getChangedAttributes()
    {
        return $this->_changed;
    }
    
    /**
     * Gets changed node data.
     *
     * The array contains all changed attributes and the DN with key 'dn'.
     * This is the format used by {@link Zend_Ldap_Helper::getAttribute()}.
     * 
     * This is an offline method.
     * 
     * @return array
     */
    public function getChangedData()
    {
        $data=$this->getChangedAttributes();
        $data['dn']=$this->getDn();
        return $data;
    }
    
    /**
     * Sets a LDAP attribute bypassing attribute checks.
     * 
     * This is an offline method.
     *
     * @param  string $name
     * @param  mixed $value
     * @param  string $encoding
     * @return Zend_Ldap_Node *Provides a fluid interface*
     * @throws Zend_Ldap_Exception 
     */
    private function _setAttribute($name, $value, $encoding='UTF-8')
    {
        Zend_Ldap_Helper::setAttribute($this->_changed, $name, $value, false, $encoding);
        return $this;
    }
    
    /**
     * Sets a LDAP attribute.
     * 
     * This is an offline method.
     *
     * @param  string $name
     * @param  mixed $value
     * @param  string $encoding
     * @return Zend_Ldap_Node *Provides a fluid interface*
     * @throws Zend_Ldap_Exception 
     */
    public function setAttribute($name, $value, $encoding='UTF-8')
    {
        $this->_checkAttribute($name, $encoding);
        return $this->_setAttribute($name, $value, $encoding);
    }
    
    /**
     * Appends to a LDAP attribute.
     * 
     * This is an offline method.
     *
     * @param  string $name
     * @param  mixed $value
     * @param  string $encoding
     * @return Zend_Ldap_Node *Provides a fluid interface*
     * @throws Zend_Ldap_Exception 
     */
    public function appendToAttribute($name, $value, $encoding='UTF-8')
    {
        $this->_checkAttribute($name, $encoding);
        $name=mb_strtolower($name, $encoding);
        if (!isset($this->_changed[$name]) && isset($this->_data[$name])) {
            $this->_changed[$name]=$this->_data[$name];
        }
        Zend_Ldap_Helper::setAttribute($this->_changed, $name, $value, true, $encoding);
        return $this;
    }
    
    /**
     * Sets a LDAP date/time attribute.
     * 
     * This is an offline method.
     *
     * @param  string $name
     * @param  integer|array $value
     * @param  bool $utc
     * @param  string $encoding
     * @return Zend_Ldap_Node *Provides a fluid interface*
     * @throws Zend_Ldap_Exception 
     */
    public function setDateTimeAttribute($name, $value, $utc=false, $encoding='UTF-8')
    {
        $this->_checkAttribute($name, $encoding);
        Zend_Ldap_Helper::setDateTimeAttribute($this->_changed, $name, $value, $utc, false, $encoding);
        return $this;
    }
    
    /**
     * Appends to a LDAP date/time attribute.
     * 
     * This is an offline method.
     *
     * @param  string $name
     * @param  integer|array $value
     * @param  bool $utc
     * @param  string $encoding
     * @return Zend_Ldap_Node *Provides a fluid interface*
     * @throws Zend_Ldap_Exception 
     */
    public function appendToDateTimeAttribute($name, $value, $utc=false, $encoding='UTF-8')
    {
        $this->_checkAttribute($name, $encoding);
        $name=mb_strtolower($name, $encoding);
        if (!isset($this->_changed[$name]) && isset($this->_data[$name])) {
            $this->_changed[$name]=$this->_data[$name];
        }
        Zend_Ldap_Helper::setDateTimeAttribute($this->_changed, $name, $value, $utc, true, $encoding);
        return $this;
    }
    
    /**
     * Sets a LDAP password.
     *
     * @param  string $password
     * @param  string $hashType
     * @param  string $attribName
     * @param  string $encoding
     * @return Zend_Ldap_Node *Provides a fluid interface*
     * @throws Zend_Ldap_Exception
     */
    public function setPasswordAttribute($password, $hashType=Zend_Ldap_Helper::PASSWORD_HASH_MD5,
        $attribName='userPassword', $encoding='UTF-8')
    {
        $this->_checkAttribute($attribName, $encoding);
        Zend_Ldap_Helper::setPassword($this->_changed, $password, $hashType, $attribName, $encoding);
        return $this;
    }
    
    /**
     * Deletes (empties) a LDAP attribute.
     * 
     * This method deletes the attribute contents without removing the attribute.
     * If the node is updated on the LDAP server the approrpiate attribute on the LDAP 
     * server gets *deleted*.
     * 
     * This is an offline method.
     *
     * @param  string $name
     * @param  string $encoding
     * @return Zend_Ldap_Node *Provides a fluid interface*
     * @throws Zend_Ldap_Exception
     */
    public function deleteAttribute($name, $encoding='UTF-8')
    {
        $this->_checkAttribute($name, $encoding);
        $name=mb_strtolower($name, $encoding);
        if (isset($this->_data[$name]) || isset($this->_changed[$name])) {
            Zend_Ldap_Helper::setAttribute($this->_changed, $name, null, false, $encoding);
        }
        return $this;
    }
    
    /**
     * Removes a LDAP attribute.
     * 
     * This method removes the attribute from the local object.
     * If the node is updated on the LDAP server the approrpiate attribute on the LDAP 
     * server remains untouched. 
     * 
     * This is an offline method.
     *
     * @param  string $name
     * @param  string $encoding
     * @return Zend_Ldap_Node *Provides a fluid interface*
     * @throws Zend_Ldap_Exception
     */
    public function removeAttribute($name, $encoding='UTF-8')
    {
        $this->_checkAttribute($name, $encoding);
        $name=mb_strtolower($name, $encoding);
        if (isset($this->_data[$name])) unset($this->_data[$name]);
        if (isset($this->_changed[$name])) unset($this->_changed[$name]);
        return $this;
    }
    
    /**
     * Checks whether a given attribute exists.
     *
     * If $emptyExists is false empty attributes (containing only array()) are
     * treated as non-existent returning false.
     * If $emptyExists is true empty attributes are treated as existent returning 
     * true. In this case method returns false only if the attribute name is 
     * missing in the key-collection.
     * 
     * @param  string $name
     * @param  boolean $emptyExists
     * @param  string $encoding
     * @return boolean
     */
    public function existsAttribute($name, $emptyExists=false, $encoding='UTF-8')
    {
        $name=mb_strtolower($name, $encoding);
        if ($name==='dn') return false;
        
        $attribs=$this->getAttributes();
        if (isset($attribs[$name])) {
            if ($emptyExists) return true;
            return count($attribs[$name])>0;
        }
        else return false;
    }
    
    /**
     * Gets a LDAP attribute.
     * 
     * This is an offline method.
     *
     * @param  string $name
     * @param  integer $index
     * @param  string $encoding
     * @return mixed 
     * @throws Zend_Ldap_Exception
     */
    public function getAttribute($name, $index=null, $encoding='UTF-8')
    {
        if ($name=='dn') return $this->getDn();
        else return Zend_Ldap_Helper::getAttribute($this->getAttributes(), $name, $index, $encoding);
    }
    
    /**
     * Gets a LDAP date/time attribute.
     * 
     * This is an offline method.
     *
     * @param  string $name
     * @param  integer $index
     * @param  string $encoding
     * @return array|integer 
     * @throws Zend_Ldap_Exception
     */
    public function getDateTimeAttribute($name, $index=null, $encoding='UTF-8')
    {
        return Zend_Ldap_Helper::getDateTimeAttribute($this->getAttributes(), $name, $index, $encoding);
    }
    
    /**
     * @param  string $name
     * @param  string $encoding
     * @return boolean
     * @throws Zend_Ldap_Exception
     */
    private function _checkAttribute($name, $encoding='UTF-8')
    {
        $name=mb_strtolower($name, $encoding);
        $dnArray=$this->getDnArray();
        $rdnKey=$dnArray[0][0];
        if ($name=='dn') throw new Zend_Ldap_Exception(null, 'DN cannot be changed.');
        else if ($name==$rdnKey) throw new Zend_Ldap_Exception(null, 
            'Cannot change attribute because it\'s part of the RDN');
        else return true;
    }
    
    /**
     * Sets a LDAP attribute.
     * 
     * This is an offline method.
     *
     * @param  string $name
     * @param  mixed $value
     * @return null
     * @throws Zend_Ldap_Exception
     */
    public function __set($name, $value)
    {
        $this->setAttribute($name, $value);
    }
    
    /**
     * Gets a LDAP attribute.
     * 
     * This is an offline method.
     *
     * @param  string $name
     * @return array 
     * @throws Zend_Ldap_Exception
     */
    public function __get($name)
    {
        return $this->getAttribute($name, null);
    }
    
    /**
     * Deletes (empties) a LDAP attribute.
     * 
     * This method deletes the attribute contents without removing the attribute.
     * If the node is updated on the LDAP server the approrpiate attribute on the LDAP 
     * server gets *deleted*.
     * 
     * This is an offline method.
     *
     * @param  string $name
     * @return null
     * @throws Zend_Ldap_Exception
     */
    public function __unset($name)
    {
        $this->deleteAttribute($name);
    }
    
    /**
     * Checks whether a given attribute exists.
     *
     * Empty attributes will be treated as non-existent. 
     * 
     * @param  string $name
     * @return boolean
     */
    public function __isset($name)
    {
        return $this->existsAttribute($name, false);
    }
    
    /**
     * Sets a LDAP attribute.
     * Implements ArrayAccess.
     * 
     * This is an offline method.
     *
     * @param  string $name
     * @param  mixed $value
     * @return null
     * @throws Zend_Ldap_Exception
     */
    public function offsetSet($name, $value)
    {
        $this->setAttribute($name, $value);
    }
    
    /**
     * Gets a LDAP attribute.
     * Implements ArrayAccess.
     * 
     * This is an offline method.
     *
     * @param  string $name
     * @return array 
     * @throws Zend_Ldap_Exception
     */
    public function offsetGet($name)
    {
        return $this->getAttribute($name, null);
    }
    
    /**
     * Deletes (empties) a LDAP attribute.
     * Implements ArrayAccess.
     * 
     * This method deletes the attribute contents without removing the attribute.
     * If the node is updated on the LDAP server the approrpiate attribute on the LDAP 
     * server gets *deleted*.
     * 
     * This is an offline method.
     *
     * @param  string $name
     * @return null
     * @throws Zend_Ldap_Exception
     */
    public function offsetUnset($name)
    {
        $this->deleteAttribute($name);
    }
    
    /**
     * Checks whether a given attribute exists.
     * Implements ArrayAccess.
     *
     * Empty attributes will be treated as non-existent. 
     * 
     * @param  string $name
     * @return boolean
     */
    public function offsetExists($name)
    {
        return $this->existsAttribute($name, false);
    }
    
    /**
     * Check if node exists on LDAP.
     *
     * This is an online method.
     * 
     * @return boolean 
     * @throws Zend_Ldap_Exception
     */ 
    public function exists() 
    { 
        return $this->getLdap()->exists($this->getDn());
    } 
    
    /**
     * Reload node attributes from LDAP.
     *
     * This is an online method.
     * 
     * @return Zend_Ldap_Node *Provides a fluid interface* 
     * @throws Zend_Ldap_Exception
     */ 
    public function reload() 
    { 
        $data=$this->getLdap()->getEntry($this->getDn(), array('*', '+'), true);
        $this->_loadData($data);
        return $this;
    }
    
    /**
     * Search current subtree with given options.
     * 
     * This is an online method.
     *
     * @param string $filter
     * @param integer $scope
     * @param array $attributes
     * @param string $sort
     * @return Zend_Ldap_QueryResult 
     * @throws Zend_Ldap_Exception
     */ 
    public function searchSubtree($filter, $scope=Zend_Ldap_Ext::SEARCH_SCOPE_SUB, 
        array $attributes=array(), $sort=null)
    {
        return $this->getLdap()->search($filter, $this->getDn(), $scope, $attributes, $sort); 
    }
    
    /**
     * Count items in current subtree found by given filter.
     *
     * This is an online method.
     * 
     * @param string $filter
     * @param integer $scope
     * @return integer 
     * @throws Zend_Ldap_Exception
     */ 
    public function countSubtree($filter, $scope=Zend_Ldap_Ext::SEARCH_SCOPE_SUB)
    {
        return $this->getLdap()->count($filter, $this->getDn(), $scope);
    }
    
    /**
     * Count children of current node.
     *
     * This is an online method.
     * 
     * @return integer 
     * @throws Zend_Ldap_Exception
     */ 
    public function countChildren()
    {
        return $this->countSubtree('(objectClass=*)', Zend_Ldap_Ext::SEARCH_SCOPE_ONE);
    }
    
    /**
     * Gets children of current node.
     * 
     * This is an online method.
     *
     * @param  string $filter
     * @param  array $attributes
     * @param  string $sort
     * @return Zend_Ldap_QueryResult 
     * @throws Zend_Ldap_Exception
     */ 
    public function searchChildren($filter, array $attributes=array(), $sort=null)
    {
        return $this->searchSubtree($filter, Zend_Ldap_Ext::SEARCH_SCOPE_ONE, $attributes, $sort);
    }
    
    /**
     * Returns the children for the current node.
     * 
     * This is an online method.
     * 
     * @return Zend_Ldap_Node_Children 
     * @throws Zend_Ldap_Exception
     */ 
    public function getChildren()
    {
        if (is_null($this->_children)) {
            $qr=$this->searchChildren('(objectClass=*)', array('*', '+'), null);
            /** 
             * Zend_Ldap_Node_Children 
             */
            require_once 'Zend/Ldap/Node/Children.php';
            $this->_children=new Zend_Ldap_Node_Children($this, $qr);
        }
        return $this->_children;
    }
    
    /**
     * Returns the parent of the current node.
     *
     * @return Zend_Ldap_Node
     * @throws Zend_Ldap_Exception
     */
    public function getParent()
    {
        $dnArray=$this->getDnArray();
        array_shift($dnArray);
        $parentDn=Zend_Ldap_Helper::implodeDn($dnArray);
        return self::fromLdap($parentDn, $this->getLdap());
    }
    
    /**
     * Checks if current node has children.
     * Returns whether the current element has children.
     * 
     * This is an online method.
     *
     * @return boolean 
     * @throws Zend_Ldap_Exception
     */
    public function hasChildren()
    {
        return ($this->countChildren()>0);
    }
    
    /**
     * Returns an Iterator for the attributes.
     *
     * @return Zend_Ldap_Node_AttributeIterator
     */
    public function getIterator()
    {
        /** 
         * Zend_Ldap_Node_AttributeIterator 
         */
        require_once 'Zend/Ldap/Node/AttributeIterator.php';
        return new Zend_Ldap_Node_AttributeIterator($this);
    }
}